package org.xteam.camllight;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.xteam.camllight.io.Channel;

public class Intern {
	
	public static final int Base_magic_number = 0x8495A6B9;
	
	public static final int Big_endian_32_magic_number    = Base_magic_number;
	public static final int Little_endian_32_magic_number = Base_magic_number + 1;
	public static final int Big_endian_64_magic_number    = Base_magic_number + 2;
	public static final int Little_endian_64_magic_number = Base_magic_number + 3;
	public static final int Compact_magic_number          = Base_magic_number + 4;
	
	public static final int First_valid_magic_number      = Base_magic_number;
	public static final int Last_valid_magic_number       = Base_magic_number + 4;
	
	public static final int PREFIX_SMALL_BLOCK = 0x80;
	public static final int PREFIX_SMALL_INT = 0x40;
	public static final int PREFIX_SMALL_STRING = 0x20;
	public static final int CODE_INT8 = 0x0;
	public static final int CODE_INT16 = 0x1;
	public static final int CODE_INT32 = 0x2;
	public static final int CODE_INT64 = 0x3;
	public static final int CODE_SHARED8 = 0x4;
	public static final int CODE_SHARED16 = 0x5;
	public static final int CODE_SHARED32 = 0x6;
	public static final int CODE_BLOCK32 = 0x8;
	public static final int CODE_STRING8 = 0x9;
	public static final int CODE_STRING32 = 0xA;
	public static final int CODE_DOUBLE = 0xB;

	public static Object internVal(Channel chan) {
		int magic = chan.getWord();
		if (magic < First_valid_magic_number || magic > Last_valid_magic_number)
			throw new RuntimeException("intern: bad object");
		if (magic == Compact_magic_number)
			return internCompactVal(chan);
		else
			return internFastVal(chan, magic);
	}

	private static Object internFastVal(Channel chan, int magic) {
		int whsize = chan.getWord();
		if (whsize == 0) {
			int res = chan.getWord();
			if (Value.isLong(res))
				return new Integer(res);
		    else
		    	return Atom.get(res >> 2);
		}
		boolean is64 = magic == Little_endian_64_magic_number
			|| magic == Big_endian_64_magic_number;
		boolean isBig = magic == Big_endian_32_magic_number
			|| magic == Big_endian_64_magic_number;
		DataReader reader = new DataReader(chan, is64, isBig, whsize);
		Map<Integer, Object> objectMap = new HashMap<Integer, Object>();
		Object firstObject = null;
		while (! reader.atEnd()) {
			int header = reader.getWord();
			int tag = Header.tagOf(header);
			int wosize = Header.woSizeOf(header);
			int offset = reader.getOffset();
			Object object = null;
			if (tag == Tags.CLOSURE_TAG) {
				throw new RuntimeException("Closure");
			} else if (tag < Tags.NO_SCAN_TAGS) {
				Block block = new Block(tag, wosize);
				for (int i = 0; i < wosize; ++i) {
					Object field = null;
					int fieldValue = reader.getWord();
					if ((fieldValue & 3) == 0) {
						int address = is64 ? fieldValue>>3 : fieldValue>>2;
						if (objectMap.containsKey(address)) {
							field = objectMap.get(address);
						} else {
							if (address > whsize) {
								throw new RuntimeException("bad pointer " + address + " > " + whsize);
							}
							field = new ForwardPointer(address);
						}
					} else if ((fieldValue & 3) == 2) {
						field = Atom.get(fieldValue>>2);
					} else {
						field = new Integer(fieldValue);
					}
					block.setField(i, field);
				}
				object = block;
			} else if (tag == Tags.STRING_TAG) {
				object = reader.getString(wosize);
			} else {
				throw new RuntimeException("tag not handled " + tag + ", size " + wosize);
			}
			if (firstObject == null) {
				firstObject = object;
			}
			objectMap.put(offset, object);
		}
		for (Object object : objectMap.values()) {
			if (object instanceof Block) {
				Block block = (Block) object;
				for (int i = 0; i < block.size(); ++i) {
					Object value = block.field(i);
					if (value instanceof ForwardPointer) {
						ForwardPointer pointer = (ForwardPointer) value;
						if (! objectMap.containsKey(pointer.address)) {
							throw new RuntimeException("unresolved pointer " + pointer.address);
						}
						Object newValue = objectMap.get(pointer.address);
						block.setField(i, newValue);
					}
				}
			}
		}
		return firstObject;
	}

	private static Object internCompactVal(Channel chan) {
		int numObjects = chan.getWord();
		int size32 = chan.getWord();
		int size64 = chan.getWord();
		int whsize = size32;
		List<Object> internTable = new ArrayList<Object>(numObjects);
		if (whsize == 0) {
		    return readCompact(chan, internTable);
		} else {
			if (Value.Wosize_whsize(whsize) > Options.Max_wosize) {
				throw new RuntimeException("intern: structure too big");
			}
			//intern_block = alloc_shr(Value.Wosize_whsize(whsize), Tags.STRING_TAG);
			//intern_header = Hd_val(intern_block);
			//intern_color = Color_hd(intern_header);
			//Assert (intern_color == White || intern_color == Black);
			//intern_ptr = (header_t *) Hp_val(intern_block);
			//obj_counter = 0;
			//intern_obj_table = (value *) stat_alloc(num_objects * sizeof(value));
			return readCompact(chan, internTable);
		}
	}
	
	private static Object readCompact(Channel chan, List<Object> intern_obj_table) {
		Object v;
		int code = chan.getch();
		if (code >= PREFIX_SMALL_INT) {
			if (code >= PREFIX_SMALL_BLOCK) {
				// Small block
				int tag = code & 0xF;
				int size = (code >> 4) & 0x7;
			    //read_block:
				if (size == 0) {
					v = Atom.get(tag);
				} else {
					Block intern_ptr = new Block(tag, size);
					intern_obj_table.add(intern_ptr);
					for(int i = 0; i < size; ++i) {
						intern_ptr.setField(i, readCompact(chan, intern_obj_table));
					}
					v = intern_ptr;
				}
			} else {
				// Small integer
				v = Value.valLong(code & 0x3F);
			}
		} else {
			if (code >= PREFIX_SMALL_STRING) {
				int len = (code & 0x1F);
				v = input_string(chan, len);
				intern_obj_table.add(v);
			} else {
				switch(code) {
				case CODE_STRING8: {
					int len = input_bytes(chan, 1, false);
					v = input_string(chan, len);
					intern_obj_table.add(v);
					break;
				}
				case CODE_STRING32: {
					int len = input_bytes(chan, 4, false);
					v = input_string(chan, len);
					intern_obj_table.add(v);
					break;
				}
				case CODE_SHARED8: {
					int ofs = input_bytes(chan, 1, false);
					v = intern_obj_table.get(intern_obj_table.size() - ofs);
					break;
				}
				case CODE_SHARED16: {
					int ofs = input_bytes(chan, 2, false);
					v = intern_obj_table.get(intern_obj_table.size() - ofs);
					break;
				}
				case CODE_SHARED32: {
					int ofs = input_bytes(chan, 4, false);
					v = intern_obj_table.get(intern_obj_table.size() - ofs);
					break;
				}
				case CODE_INT8:
					v = Value.valLong(input_bytes(chan, 1, true));
					break;
					
				case CODE_INT16:
					v = Value.valLong(input_bytes(chan, 2, true));
					break;
					
				case CODE_BLOCK32:
					int header = input_bytes(chan, 4, false);
					int tag = Header.tagOf(header);
					int size = Header.woSizeOf(header);
					if (size == 0) {
						v = Atom.get(tag);
					} else {
						Block intern_ptr = new Block(tag, size);
						intern_obj_table.add(intern_ptr);
						for(int i = 0; i < size; ++i) {
							intern_ptr.setField(i, readCompact(chan, intern_obj_table));
						}
						v = intern_ptr;
					}
					break;
					
				default:
					throw new RuntimeException("not completed " + code);
				}
			}
		}
		return v;
	}

	private static String input_string(Channel chan, int len) {
		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < len; ++i) {
			buffer.append((char)chan.getch());
		}
		return buffer.toString();
	}

	private static int input_bytes(Channel chan, int nbytes, boolean sign_extend) {
		int res = chan.getch();
		if (sign_extend) {
			res = (res << ((Value.SIZEOF_VALUE - 1) * 8)) >> ((Value.SIZEOF_VALUE - 1) * 8);
		}
		for (int i = 1; i < nbytes; i++) {
			res = (res << 8) + chan.getch();
		}
		return res;
	}

	private static class ForwardPointer {

		private int address;

		public ForwardPointer(int address) {
			this.address = address;
		}
		
	}
	
	private static class DataReader {

		private Channel chan;
		private boolean is64;
		private boolean isBig;
		private int wordSize;
		private int offset;

		public DataReader(Channel chan, boolean is64, boolean isBig, int wordSize) {
			this.chan = chan;
			this.is64 = is64;
			this.isBig = isBig;
			this.offset = 0;
			this.wordSize = wordSize;
		}
		
		public Object getString(int wosize) {
			StringBuffer buffer = new StringBuffer();
			for (int i = 0; i < wosize*getWordSize()-1; ++i) {
				buffer.append((char)chan.getch());
			}
			int size = chan.getch();
			buffer.setLength(wosize*getWordSize()-size-1); // skip final '\0'
			offset += wosize;
			return buffer.toString();
		}

		public int getOffset() {
			return offset;
		}

		public boolean atEnd() {
			return offset >= wordSize;
		}
		
		private int getWordSize() {
			return is64 ? 8 : 4;
		}

		public int getWord() {
			int size = getWordSize();
			long value = 0;
			if  (isBig) {
				for (int i = 0; i < size; ++i) {
					value = (value<<8) | chan.getch();
				}
			} else {
				for (int i = 0; i < size; ++i) {
					value |= chan.getch()<<(i*8);
				}
			}
			if (value > 0xFFFFFFFFL) {
				throw new RuntimeException("word too big");
			}
			offset += 1;
			return (int) value;
		}
		
	}
	
}
